﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using PhiStore.Policies;
using PhiStore.Storage;
using PhiStore.Util;

namespace PhiStore.ObjectManaging
{
    class ObjectDevice : IObjectDevice
    {
        private Dictionary<string, HashSet<string>> uncommittedChanges;

        private IStorageDevice storage;
        public ObjectDevice(IStorageDevice device)
        {
            this.storage = device;
            uncommittedChanges = new Dictionary<string, HashSet<string>>();
        }

        private const string policyName = "pol";
        private const string objectInfoName = "nfo";
        private const string uncommittedSuffix = ".bak";
        private const string objectCreateUncommitted = "uncommitted";
        private const string objectIndexCreateUncommittedSuffix = ".new";

        private long score = -1L;

        #region Create

        public ReturnCode CreateObject(string key, Policy policy, ObjectInformation info, bool awaitCommit)
        {
            ReturnCode result = ReturnCode.Failure;

            string infopath = key + storage.DirectorySeperator + objectInfoName;
            string policypath = key + storage.DirectorySeperator + policyName;

            string uncommittedObject = key + storage.DirectorySeperator + objectCreateUncommitted;

            if (!storage.Exist(key))
            {
                if (storage.Create(key + storage.DirectorySeperator) &&
                    storage.Write(uncommittedObject, new byte[0]) &&
                    storage.Write(infopath, Serialize(info)) &&
                    storage.Write(policypath, Serialize(policy)))
                {
                    AddUncommitted(key, uncommittedObject);
                    result = ReturnCode.Success;
                }
                else
                    result = ReturnCode.DeviceOperationFailed;
            }
            else
                result = ReturnCode.ObjectAlreadyExist;


            if (result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Creation failed due to: " + result.ToString());
                PhiServer.Instance.Stop();
            }
            else if (!awaitCommit)
                Commit(key);


            return result;
        }

        public ReturnCode CreateObject(string key, long index, byte[] obj, bool awaitCommit)
        {
            ReturnCode result = ReturnCode.Failure;

            string path = key + storage.DirectorySeperator + index.ToString();

            if (storage.Exist(key) && !storage.Exist(path))
            {
                string uncommittedPath = path + objectIndexCreateUncommittedSuffix;
                if (storage.Write(uncommittedPath, new byte[0]) && storage.Write(path, obj))
                {
                    AddUncommitted(key, uncommittedPath);
                    result = ReturnCode.Success;
                }
                else
                    result = ReturnCode.DeviceOperationFailed;
            }
            else
                result = ReturnCode.ObjectAlreadyExist;

            if (result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Creation @ " + index.ToString() + " failed due to: " + result.ToString());
                PhiServer.Instance.Stop();
            }
            else if (!awaitCommit)
                Commit(key);

            return result;
        }

        #endregion

        #region Read
        public byte[] ReadObject(string key, long index)
        {
            byte[] result = null;

            ReturnCode _result = ReturnCode.Failure;
            string path = key + storage.DirectorySeperator + index.ToString();

            if (storage.Exist(key) && storage.Exist(path))
            {
                result = storage.Read(path);
                if (result != null)
                    _result = ReturnCode.Success;
                else
                    _result = ReturnCode.DeviceOperationFailed;
            }
            else
                _result = ReturnCode.ObjectNotFound;

            if (_result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Reading @" + index.ToString() + " failed due to: " + _result.ToString());
                PhiServer.Instance.Stop();
            }

            return result;
        }

        public Policy ReadPolicy(string key)
        {
            Policy result = null;
            ReturnCode _result = ReturnCode.Failure;
            string path = key + storage.DirectorySeperator + policyName;

            if (storage.Exist(key) && storage.Exist(path))
            {
                byte[] policyBytes = storage.Read(path);
                if (policyBytes != null)
                {
                    _result = ReturnCode.Success;
                    result = Deserialize<Policy>(policyBytes);
                }
                else
                    _result = ReturnCode.DeviceOperationFailed;
            }
            else
                _result = ReturnCode.ObjectNotFound;

            if (_result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Policy Reading failed due to: " + _result.ToString());
                PhiServer.Instance.Stop();
            }

            return result;
        }

        public ObjectInformation ReadObjectInformation(string key)
        {
            ObjectInformation result = null;
            ReturnCode _result = ReturnCode.Failure;
            string path = key + storage.DirectorySeperator + objectInfoName;

            if (storage.Exist(key) && storage.Exist(path))
            {
                byte[] oiBytes = storage.Read(path);
                if (oiBytes != null)
                {
                    _result = ReturnCode.Success;
                    result = Deserialize<ObjectInformation>(oiBytes);
                }
                else
                    _result = ReturnCode.DeviceOperationFailed;
            }
            else
                _result = ReturnCode.ObjectNotFound;

            if (_result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Information Reading failed due to: " + _result.ToString());
                PhiServer.Instance.Stop();
            }

            return result;
        }

        #endregion

        #region Update
        public ReturnCode UpdateObject(string key, long index, byte[] obj, bool awaitCommit)
        {
            ReturnCode result = ReturnCode.Failure;
            string path = key + storage.DirectorySeperator + index.ToString();

            if (storage.Exist(key) && storage.Exist(path))
            {
                string bakpath = path + uncommittedSuffix;
                if (storage.Rename(path, bakpath))
                {
                    AddUncommitted(key, bakpath);
                    if (storage.Write(path, obj))
                        result = ReturnCode.Success;
                    else
                        result = ReturnCode.DeviceOperationFailed;
                }
                else
                    result = ReturnCode.DeviceOperationFailed;
            }
            else
                result = ReturnCode.ObjectNotFound;

            if (result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Updating @ " + index.ToString() + " failed due to: " + result.ToString());
                PhiServer.Instance.Stop();
            }
            else if (!awaitCommit)
                Commit(key);

            return result;
        }

        public ReturnCode UpdateObjectInfo(string key, ObjectInformation info, bool awaitCommit)
        {
            ReturnCode result = ReturnCode.Failure;
            string path = key + storage.DirectorySeperator + objectInfoName;

            if (storage.Exist(key) && storage.Exist(path))
            {
                string bakpath = path + uncommittedSuffix;
                if (ContainsUncommitted(key, path) //Already had a change for other info
                    || storage.Rename(path, bakpath))
                {
                    AddUncommitted(key, bakpath);
                    if (storage.Write(path, Serialize(info)))
                        result = ReturnCode.Success;
                    else
                        result = ReturnCode.DeviceOperationFailed;
                }
                else
                    result = ReturnCode.DeviceOperationFailed;
            }
            else
                result = ReturnCode.ObjectNotFound;

            if (result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Information Updating failed due to: " + result.ToString());
                PhiServer.Instance.Stop();
            }
            else if (!awaitCommit)
                Commit(key);

            return result;
        }

        #endregion

        #region Delete
        /// <summary>
        /// Completely removes object
        /// </summary>
        /// <param name="key"></param>
        /// <param name="awaitCommit"></param>
        /// <returns></returns>
        public ReturnCode DeleteObject(string key, bool awaitCommit)
        {
            ReturnCode result = ReturnCode.Failure;
            string path = key + storage.DirectorySeperator;

            if (storage.Exist(key))
            {
                string bakpath = key + uncommittedSuffix + storage.DirectorySeperator;
                if (storage.Rename(path, bakpath))
                {
                    AddUncommitted(key, bakpath);
                    result = ReturnCode.Success;
                }
                else
                    result = ReturnCode.DeviceOperationFailed;
            }
            else
                result = ReturnCode.ObjectNotFound;

            if (result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Deletion failed due to: " + result.ToString());
                PhiServer.Instance.Stop();
            }
            else if (!awaitCommit)
                Commit(key);

            return result;
        }

        public ReturnCode DeleteObject(string key, long index, bool awaitCommit)
        {
            ReturnCode result = ReturnCode.Failure;

            string path = key + storage.DirectorySeperator + index.ToString();

            if (storage.Exist(path))
            {
                string bakpath = path + uncommittedSuffix;
                if (storage.Rename(path, bakpath))
                {
                    AddUncommitted(key, bakpath);
                    result = ReturnCode.Success;
                }
                else
                    result = ReturnCode.DeviceOperationFailed;
            }
            else
                result = ReturnCode.ObjectNotFound;

            if (result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Deletion @ " + index.ToString() + " failed due to: " + result.ToString());
                PhiServer.Instance.Stop();
            }
            else if (!awaitCommit)
                Commit(key);

            return result;
        }

        public ReturnCode Rename(string key, long oldIndex, long newIndex, bool awaitCommit)
        {
            ReturnCode result = ReturnCode.Failure;

            string oldpath = key + storage.DirectorySeperator + oldIndex.ToString();
            string newpath = key + storage.DirectorySeperator + newIndex.ToString();

            if (!storage.Exist(newpath))
            {
                if (storage.Exist(oldpath))
                {
                    string oldBakPath = oldpath + uncommittedSuffix;
                    byte[] copy = storage.Read(oldpath);
                    if (copy != null && storage.Write(newpath, copy) && storage.Rename(oldpath, oldBakPath))
                    {
                        AddUncommitted(key, oldBakPath);
                        result = ReturnCode.Success;
                    }
                    else
                        result = ReturnCode.DeviceOperationFailed;
                }
                else
                    result = ReturnCode.ObjectNotFound;
            }
            else
                result = ReturnCode.ObjectAlreadyExist;


            if (result != ReturnCode.Success)
            {
                Log.Instance.AddLogEntry(storage.ToString(), "Object Renaming @ " + oldIndex.ToString() + " failed due to: " + result.ToString());
                PhiServer.Instance.Stop();
            }
            else if (!awaitCommit)
                Commit(key);

            return result;
        }

        #endregion

        public bool RangeExist(string key, long from, long to)
        {
            bool exist = true;
            for (long i = from; i < to && exist; i++)
            {
                exist &= Exist(key, i);
            }
            return exist;
        }

        public bool Exist(string key, long index)
        {
            string path = key + storage.DirectorySeperator + index.ToString();
            return storage.Exist(path);
        }

        public bool Exist(string key)
        {
            return storage.Exist(key);
        }

        public void Commit(string key)
        {
            ReturnCode result = ReturnCode.Success;

            List<string> uncommittedPaths = new List<string>();
            if (uncommittedChanges.ContainsKey(key))
                uncommittedPaths.AddRange(uncommittedChanges[key]);

            foreach (string path in uncommittedPaths)
            {
                bool succes = false;
                if (path.EndsWith(storage.DirectorySeperator))
                    succes = storage.DeleteDirectory(path);
                else
                    succes = storage.Delete(path);

                if (succes)
                    result = ReturnCode.Success;
                else
                    result = ReturnCode.DeviceOperationFailed;
            }

            if (result != ReturnCode.Success)
                Log.Instance.AddLogEntry(storage.ToString(), "Commit failed due to: " + result.ToString());
            else
                uncommittedChanges.Remove(key);
        }

        public void Abort(string key)
        {
            ReturnCode result = ReturnCode.Failure;

            List<string> uncommittedPaths = new List<string>();
            if (uncommittedChanges.ContainsKey(key))
                uncommittedPaths.AddRange(uncommittedChanges[key]);

            foreach (string path in uncommittedPaths)
            {
                if (path.EndsWith(objectCreateUncommitted))
                {
                    string Cpath = path.Replace(storage.DirectorySeperator + objectCreateUncommitted, "");
                    result = storage.DeleteDirectory(Cpath) ? ReturnCode.Success : ReturnCode.DeviceOperationFailed;
                }
                else if (path.EndsWith(objectIndexCreateUncommittedSuffix))
                {
                    string Cpath = path.Replace(objectIndexCreateUncommittedSuffix, "");
                    if (storage.Exist(Cpath))
                    {
                        result = storage.Delete(Cpath) ? ReturnCode.Success : ReturnCode.DeviceOperationFailed;
                    }
                }
                else
                {
                    string Cpath = path.Replace(uncommittedSuffix, "");
                    if (storage.Exist(Cpath))
                    {
                        if (Cpath.EndsWith(storage.DirectorySeperator))
                        {
                            result = ReturnCode.ObjectAlreadyExist; //this is bad!
                        }
                        else
                        {
                            storage.Delete(Cpath);
                        }
                    }

                    if (!storage.Exist(Cpath))
                    {
                        if (storage.Rename(path, Cpath))
                            result = ReturnCode.Success;
                        else
                            result = ReturnCode.DeviceOperationFailed;
                    }
                }
            }

            if (result != ReturnCode.Success)
                Log.Instance.AddLogEntry(storage.ToString(), "Abort failed due to: " + result.ToString());
        }

        public Dictionary<string, ObjectLock> GetObjects()
        {
            HashSet<string> files = new HashSet<string>(storage.GetPaths());

            //Checking for uncommitted changes - reverting
            foreach (string file in files)
            {
                if (file.EndsWith(uncommittedSuffix) || //object update / index delete
                    file.EndsWith(objectCreateUncommitted) || //object create
                    file.EndsWith(objectIndexCreateUncommittedSuffix) //object append/create index
                    )
                {
                    string key = file.Substring(0, file.LastIndexOf(storage.DirectorySeperator) - 1);
                    AddUncommitted(key, file);
                }
                else if (file.EndsWith(uncommittedSuffix + storage.DirectorySeperator)) //complete object delete
                {
                    string key = file.Substring(0, file.LastIndexOf(uncommittedSuffix + storage.DirectorySeperator) - 1);
                    AddUncommitted(key, file);
                }
            }
            string[] keys = new string[uncommittedChanges.Keys.Count];
            uncommittedChanges.Keys.CopyTo(keys, 0);
            foreach (string key in keys)
            {
                Abort(key); //abort all uncommitted!
            }

            List<string> objNames = new List<string>();
            files = new HashSet<string>(storage.GetPaths()); //these should all be good!

            foreach (string file in files)
            {
                if (file.EndsWith(storage.DirectorySeperator)) //object Name
                {
                    objNames.Add(file.Remove(file.Length - 1));
                }
            }


            //traverse the objects
            Dictionary<string, ObjectLock> objects = new Dictionary<string, ObjectLock>();
            foreach (string obj in objNames)
            {
                string policypath = obj + storage.DirectorySeperator + policyName;
                string objInfoPath = obj + storage.DirectorySeperator + objectInfoName;
                if (files.Contains(policypath) //policy exist
                    && files.Contains(objInfoPath)) //stats exist
                {
                    ObjectLock oe = new ObjectLock();
                    oe.Device = this;
                    oe.Policy = ReadPolicy(obj);
                    oe.ObjectInformation = ReadObjectInformation(obj);

                    bool ObjectInformationValid = oe.ObjectInformation != null;
                    for (long i = 0; ObjectInformationValid && i < oe.ObjectInformation.Count; i++)
                    {
                        long index = oe.ObjectInformation.FirstIndex + 1;
                        ObjectInformationValid &= storage.Exist(obj + storage.DirectorySeperator + index.ToString());
                    }

                    if (ObjectInformationValid)
                        objects.Add(obj, oe);
                    else //partial commit (?) object is not in a valid state!
                        DeleteObject(obj, false);
                }
            }
            return objects;
        }

        public long Score
        {
            get
            {
                if (score < 0L)
                    Benchmark();
                return score;
            }
        }

        public void Benchmark()
        {

            // 1 MB Random data
            byte[] random = new byte[1024 * 1024];
            Random rand = new Random();
            rand.NextBytes(random);
            Stopwatch watch = new Stopwatch();
            byte[] read = null;
            bool works = storage.Works();
            try
            {
                watch.Start();
                works &= storage.Write("BENCHMARK1", random);
                works &= null != (read = storage.Read("BENCHMARK1"));
                works &= storage.Rename("BENCHMARK1", "BENCHMARK2");
                works &= storage.Delete("BENCHMARK2");
                works &= !storage.Exist("BENCHMARK1") && !storage.Exist("BENCHMARK2");
                watch.Stop();

                if (works)
                    score = 4L * 1024L * 1024L * 1024L / watch.ElapsedMilliseconds;

            }
            catch
            {
                works = false;
                Log.Instance.AddLogEntry(storage.ToString(), " failed to benchmark");
            }

            if (!Util.Compare.ArrayEquals<byte>(read, random))
            {
                Log.Instance.AddLogEntry(storage.ToString(), " failed to read what was written during benchmark");
            }

            if (!works)
            {
                Log.Instance.AddLogEntry(storage.ToString(), " doesn't work as expected");
            }
        }

        public bool InternalDeviceEquals(IStorageDevice dev)
        {
            return storage.Equals(dev);
        }

        public bool HighWaterMarkReached
        {
            get
            {// less than 5 percent remaining
                return (Convert.ToDouble(storage.RemainingBytes) / Convert.ToDouble(storage.ReservedBytes)) < 0.05;
            }
        }

        public DateTime LatestAccess(string key)
        {
            return storage.LastAccess(key);
        }

        #region helpers
        private static BinaryFormatter formatter = new BinaryFormatter();
        private static byte[] Serialize(object o)
        {
            MemoryStream ms = new MemoryStream();
            formatter.Serialize(ms, o);
            ms.Position = 0;
            byte[] result = StreamUtil.Read(ms);
            ms = null;
            return result;
        }
        private static T Deserialize<T>(byte[] bytes)
        {
            MemoryStream ms = new MemoryStream();
            StreamUtil.Copy(bytes, ms);
            ms.Position = 0;
            T result = (T)formatter.Deserialize(ms);
            ms = null;
            return result;
        }

        private void AddUncommitted(string key, string uncommittedPath)
        {
            if (!uncommittedChanges.ContainsKey(key))
                uncommittedChanges.Add(key, new HashSet<string>());

            if (!uncommittedChanges[key].Contains(uncommittedPath)) //already noted as uncommitted
                uncommittedChanges[key].Add(uncommittedPath);
        }

        private bool ContainsUncommitted(string key, string uncommittedPath)
        {
            return uncommittedChanges.ContainsKey(key) && uncommittedChanges[key].Contains(uncommittedPath);
        }

        #endregion

        public int CompareTo(ObjectDevice other)
        {
            return this.score.CompareTo(other.Score);
        }
    }

    interface IObjectDevice : IComparable<ObjectDevice>
    {
        ReturnCode CreateObject(string key, Policy policy, ObjectInformation info, bool awaitCommit);
        ReturnCode CreateObject(string key, long index, byte[] obj, bool awaitCommit);

        byte[] ReadObject(string key, long index);
        Policy ReadPolicy(string key);
        ObjectInformation ReadObjectInformation(string key);
        ReturnCode UpdateObject(string key, long index, byte[] obj, bool awaitCommit);
        ReturnCode UpdateObjectInfo(string key, ObjectInformation info, bool awaitCommit);
        ReturnCode DeleteObject(string key, bool awaitCommit);
        ReturnCode DeleteObject(string key, long index, bool awaitCommit);
        ReturnCode Rename(string key, long oldIndex, long newIndex, bool awaitCommit);

        void Commit(string key);
        void Abort(string key);

        bool RangeExist(string key, long from, long to);
        bool Exist(string key, long index);
        bool Exist(string key);

        Dictionary<string, ObjectLock> GetObjects();

        long Score { get; }
        void Benchmark();

        bool InternalDeviceEquals(IStorageDevice dev);

        bool HighWaterMarkReached { get; }
        DateTime LatestAccess(string key);
    }
}


